SpringBoot+Shior权限管理是许多项目都要使用的技术
0x01 导入依赖
在maven构建的项目pom.xml中加入如下依赖1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26<!-- 版本号 -->
<properties>
<shiro.version>1.3.0</shiro.version>
<thymeleaf-shiro.version>1.2.1</thymeleaf-shiro.version>
<shiro-ehcache.version>1.2.5</shiro-ehcache.version>
</properties>
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- [thymeleaf扩展shiro标签]使用springboot推荐的thymeleaf,未使用jsp,jsp则不需要引入此依赖 -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>${thymeleaf-shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro-ehcache.version}</version>
</dependency>
0x02 用户-角色-权限-Entity
一共3个Entity,会生成5张表.tb_user<–>tb_user_role<–>tb_role<–>tb_role_permission<–>tb_permission
注解较多,如果看不懂搜一下lombok吧,entity神器.
或者也可以自己删掉无用注解,手动写getter setter方法
User表:(由于是后台管理系统,所以我的表名为manager),相应更改即可
1 |
|
Role表1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(name = "tb_role")
(AccessType.FIELD)
(chain = true)
public class Role extends BaseUUIDEntity implements Serializable {
private static final long serialVersionUID = 1L;
(name = "roleName", length = 50)
("角色名")
private String roleName;
(length = 100)
private String description; // 角色描述,UI界面显示使用
(name="tb_role_permission",joinColumns={(name="role_id")},inverseJoinColumns={(name="permission_id")})
private Set<Permission> permissionSet = new HashSet<>();// 一个角色对应多个权限
(name = "tb_manager_role", joinColumns = { (name = "role_id") }, inverseJoinColumns = { (name = "manager_id") })
private Set<Manager> managerSet = new HashSet<>();// 一个角色对应多个用户
}
Permission表1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(name = "tb_permission")
(AccessType.FIELD)
(chain = true)
public class Permission extends BaseAutoIdEntity implements Serializable {
private static final long serialVersionUID = 1L;
(length = 50)
("权限名")
private String name;
(length = 100)
private String url;//资源路径.
(length = 50)
private String permission; //权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view
(name="tb_role_permission",joinColumns={(name="permission_id")},inverseJoinColumns={(name="role_id")})
private Set<Role> roleSet = new HashSet<>();// 一个权限对应一个角色
}
0x03 Shiro配置
由于SpringBoot不建议使用xml所以直接使用bean的方式配置ShiroConfiguration1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
//使用了lombok的Slf4j日志注解,不需要删掉即可,
4j
public class ShiroConfiguration {
/**
* Ehcache缓存,不开启的话每次都会去数据库读取权限
*
* @return
*/
public EhCacheManager getEhCacheManager() {
EhCacheManager em = new EhCacheManager();
em.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return em;
}
/**
* 自定义Realm
*
* @param cacheManager
* @return
*/
public MyShiroRealm myShiroRealm(EhCacheManager cacheManager) {
MyShiroRealm realm = new MyShiroRealm();
realm.setCacheManager(cacheManager);
return realm;
}
/**
* 创建默认WebSecurityManager
*
* @param myShiroRealm
* @return
*/
public DefaultWebSecurityManager getDefaultWebSecurityManager(MyShiroRealm myShiroRealm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(myShiroRealm);
//用户授权/认证信息Cache, 采用EhCache 缓存
defaultWebSecurityManager.setCacheManager(getEhCacheManager());
return defaultWebSecurityManager;
}
/**
* ShiroFilter
*
* @param securityManager
*/
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的连接
shiroFilterFactoryBean.setSuccessUrl("/");
//无权限跳转的页面
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
//加载shiroFilter权限控制规则
loadShiroFilterChain(shiroFilterFactoryBean);
return shiroFilterFactoryBean;
}
/**
* 加载shiroFilter权限控制规则(从数据库读取然后配置)
*/
private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean) {
/////////////////////// 下面这些规则配置最好配置到配置文件中 ///////////////////////
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
// anon:它对应的过滤器里面是空的,什么都没做
// user: 配置记住我或认证通过可以访问的地址
log.info("##################从数据库读取权限规则,加载到shiroFilter中##################");
//建议写到配置文件中(这里为了方便新手跑起来这个示例故直接写)
//anon 匿名用户 不拦截
//logout Shiro自己实现的登出功能
//authc 需要授权才能访问
//静态资源不需要拦截 /resources/** anon
//过滤链定义,从上向下顺序执行,一般将 /**放在最为下边
filterChainDefinitionMap.put("/resources/**", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/favicon.ico", "anon");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
}
/**
* shiro AOP支持, 支持权限控制,不加shiro注解则无效
*
* @param securityManager
* @return
*/
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
aasa.setSecurityManager(securityManager);
return aasa;
}
/**
* 开启Dialect,否则thymeleaf-extras-shiro标签无效
*
* @return
*/
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
}
0x04 MyShiroRealm
ShiroConfiguration配置中需要使用自定义的Realm实现授权-认证的细节1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58//使用了lombok的Slf4j日志注解,不需要删掉即可,
4j
public class MyShiroRealm extends AuthorizingRealm {
private ManagerRepository managerRepository;
/**
* 登录认证
*/
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authenticationToken) throws AuthenticationException {
//UsernamePasswordToken对象用来存放提交的登录信息
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
log.info("验证当前Subject时获取到token为:" + ReflectionToStringBuilder.toString(token, ToStringStyle.MULTI_LINE_STYLE));
//查出是否有此用户
Manager manager = managerRepository.findByUserName(token.getUsername());
if (manager != null) {
// 若存在,将此用户存放到登录认证info中,无需自己做密码对比,Shiro会为我们进行密码对比校验
return new SimpleAuthenticationInfo(manager.getUserName(), manager.getPassWord(), getName());
}
return null;
}
/**
* 权限认证,为当前登录的Subject授予角色和权限
*
* @see 经测试:本例中该方法的调用时机为需授权资源被访问时
* @see 经测试:并且每次访问需授权资源时都会执行该方法中的逻辑,这表明本例中默认并未启用AuthorizationCache
* @see 经测试:如果连续访问同一个URL(比如刷新),该方法不会被重复调用,Shiro有一个时间间隔(也就是cache时间,在ehcache-shiro.xml中配置),超过这个时间间隔再刷新页面,该方法会被执行
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("##################执行Shiro权限认证##################");
//获取当前登录输入的用户名,等价于(String) principalCollection.fromRealm(getName()).iterator().next();
String loginName = (String) super.getAvailablePrincipal(principalCollection);
//到数据库查是否有此对象
Manager manager = managerRepository.findByUserName(loginName);// 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
if (manager != null) {
//权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//用户的角色集合
Set<String> set = new HashSet<String>();
for (Role role : manager.getRoleList()) {
set.add(role.getRoleName());
}
info.setRoles(set);
//用户的角色对应的所有权限,如果只使用角色定义访问权限,下面的四行可以不要
List<Role> roleList = manager.getRoleList();
for (Role role : roleList) {
for(Permission p:role.getPermissionSet()){
info.addStringPermission(p.getPermission());
}
}
return info;
}
// 返回null的话,就会导致任何用户访问被拦截的请求时,都会自动跳转到unauthorizedUrl指定的地址
return null;
}
}
0x05 测试
数据库中 用户 角色 权限,5张表表分别增加数据;若结构不同,自己更改1
2
3
4
5
6
7INSERT INTO `flower`.`tb_manager` (`id`, `create_time`, `update_time`, `pass_word`, `phone`, `status`, `user_name`, `real_name`) VALUES ('1', NULL, NULL, '202cb962ac59075b964b07152d234b70', '13688031263', 'ENABLE', '123', '管理员');
INSERT INTO `flower`.`tb_role` (`id`, `create_time`, `update_time`, `roleName`, `description`) VALUES ('1', NULL, NULL, 'admin', '系统管理员');
INSERT INTO `flower`.`tb_role` (`id`, `create_time`, `update_time`, `roleName`, `description`) VALUES ('2', NULL, NULL, 'vip', 'VIP会员');
INSERT INTO `flower`.`tb_role` (`id`, `create_time`, `update_time`, `roleName`, `description`) VALUES ('3', NULL, NULL, 'member', '业务员');
INSERT INTO `flower`.`tb_manager_role` (`role_id`, `manager_id`) VALUES ('1', '1');
INSERT INTO `flower`.`tb_permission` (`id`, `create_time`, `update_time`, `name`, `permission`, `url`) VALUES ('1', NULL, NULL, '订单列表', 'order:view', '/order/');
INSERT INTO `flower`.`tb_role_permission` (`permission_id`, `role_id`) VALUES ('1', '2');
登陆认证
1 | (value = "/login", method = RequestMethod.POST) |
权限测试1
2
3
4
5
6("order:view")
(value = "/order/", method = RequestMethod.GET)
public ModelAndView orders(PageHelper pageHelper) {
ModelAndView mav = getMav("order/list");
return mav;
}
若数据库中没有设置对应的权限则会报如下Exception,并跳转至ShiroConfiguration中设置的/403页面1
Caused by: org.apache.shiro.authz.AuthorizationException
0x06 其他
页面标签绑定显示数据则用thymeleaf-extras-shiro
github链接:thymeleaf-extras-shiro